Developers.IO ブログで日本語の辞書を作ってみた
こんにちは。コンサルティング部のキムです :D
今回は私の日本語の勉強に参考になれるカスタマイズされた日本語の辞書を実装してみましたので、その内容をブログ化したいと思います。
目次
背景
私がクラメソに入社して日本で働いてからももう4ヶ月が経っています。初めての海外生活で大変なこともありましたが、順調に新しい生活にも慣れています。
ですが、やはり日本語はまだ難しいですね。
特にエンジニアとして技術の議論はおろか、技術に関わっている言葉や表現等分からないことが多いため、社内で常に起こっている技術的な議論やプレゼンテーション時に私だけがその内容を聞き取れなかったり、お客様とのやり取りの対応が出来なかったりします。これは会社的にも自分にもよくないので、これからは日本語の勉強に集中しようと思っています。
なので、より効果的な日本語の勉強方法について悩んだあと思い付いたのが技術的なコンテキストでよく使われる言葉や表現等を纏めて勉強すればということでした。ですが、そんなことを探すのも手強いですよね。
そう思っているうちに自分で作ってみれば?という面白い発想が思いつきました。
具体的には私の仕事に一番関わっている弊社で提供しているこのDevelopers.IOのブログの内容を活かせて、そのブログの日本語を学ぼうということです。結果としてDynamoDBのテーブルにこのようなカスタマイズされた日本語の辞書DBが出来上がりました!
正直言うと、ただ面白いからという理由だけではあまりやる気が出なかったのですが、 これからずっと改善しながらDevOps辺りの技術を実際にこのプロジェクトベースでやりたいところもあるし、 自分自身で勉強ツールを作って勉強すればもっと楽しく勉強できるところもあって作ることになりました。 あ、それに最後にブログのネタにもなるんですね(笑)
やったこと
自分のカスタマイズされた日本語の辞書を実装する為にやったことは以下のようです。
- ブログURLをインプットとしてブログの内容のテキストのみ抽出
- 抽出したテキストの形態素分析
- JSONファイルで保存
- JSONファイルのデータをDynamoDBにアップロード
- クエリを実行・結果確認
0. 前提・準備事項
仕事に直接関わることではないので実装時間はなるべく短くしたかったです。 なので、コードの再利用や厳密なテスト等は略して進めました。 最初は3時間ほどで十分ではないかと思いましたが、結果としては30分オーバーして3時間30分で作りました。
開発環境は Mac OS 上で python3.7 を用いて構成しました。 以下のようにターミナルで新しいフォルダを作って virtualenv を作ります。
$ mkdir japanese-study-proj $ cd japanese-study-proj $ virtualenv venv $ source venv/bin/activate
以下のようにプロジェクトフォルダの構成を作ります。ターミナルの (venv) 表示は略します。
$ mkdir src $ mkdir generated
必要な package をインストールしておきます。
$ pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz" $ pip install beautifulsoup4 $ pip install requests $ pip install boto3
一番上の ginza ライブラリーに関しましては後で説明させて頂きます。
1. ブログURLをインプットとしてブログの内容のテキストのみ抽出
まず、src フォールだに移動して parse.py ファイルを作ります。
$ cd src $ touch parse.py
parse.py
#!/usr/bin/python # -*- coding: utf-8 -*- from bs4 import BeautifulSoup import requests def get_html_doc(url): response = requests.get(url) return response.text def extract_all_tags(raw_content, tag_name): tag_list = raw_content.findAll(tag_name) for tag in tag_list: tag.extract() return raw_content def get_blog_content(html_doc): soup = BeautifulSoup(html_doc, 'html.parser') raw_content = soup.find('div', class_='single_article_contents') raw_content = extract_all_tags(raw_content, 'pre') raw_content = extract_all_tags(raw_content, 'script') raw_content = extract_all_tags(raw_content, 'img') raw_content = extract_all_tags(raw_content, 'iframe') content = raw_content.get_text() return content if __name__ == '__main__': url = input('Dev.IO Page URL : ') html_doc = get_html_doc(url) content = get_blog_content(html_doc) print(content)
このファイルを実行してみると、このような結果が出力されます。(一部だけスクリンショット)
2. 抽出したテキストの形態素分析
本プロジェクトのメインである日本語の形態素分析のライブラリーをご紹介いたします。
GINZAというライブラリーですが、spacyを活用しています。
私もほぼ今回初めて弄ってみましたが、凄く簡単に活用できました。サンプルコードを読むことで10分ぐらいで理解できるほど簡単なライブラリーでした。(中身は全然分からないですが)
analyze.py と vocabulary.py ファイルを作成しました。 インポートの為 init.py ファイルも作っておきました。
$ touch __init__.py $ touch analyze.py $ touch vocabulary.py
analyze.py
#!/usr/bin/python # -*- coding: utf-8 -*- import spacy import re from vocabulary import Vocabulary def create_vocabulary_list(content): nlp = spacy.load('ja_ginza') doc = nlp(content) vocab_list = [] for sentence in doc.sents: for token in sentence: if token.pos_ == 'PUNCT' or token.pos_ == 'ADP' or token.pos_ == 'SCONJ' or token.pos_ == 'NUM': continue elif token.pos_ == 'AUX' and token.dep_ == 'aux': continue elif re.match("^[A-Za-z]*$", token.orth_): continue else: vocab = Vocabulary(token, sentence) vocab_list.append(vocab) return vocab_list if __name__ == '__main__': content = '本エントリでは、サブスクリプションフィルタを利用せず、CloudWatch Logsのロググループを、S3バケットにエクスポートしてみたいと思います。' vocab_list = create_vocabulary_list(content) print(vocab_list[0].__dict__)
vocabulary.py
#!/usr/bin/python # -*- coding: utf-8 -*- import datetime import json class Vocabulary: def __init__(self, token, sentence): self.lemma = token.lemma_ # 頂く self.orth = token.orth_ # 頂き self.pos = token.pos_ # AUX self.sentence = str(sentence) # お話させて頂きました self.timestamp = datetime.datetime.utcnow().isoformat() def toJson(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
これだけ見るとコレナニ?!ということになりがちだと思い、コードを説明させて頂きます。
nlp = spacy.load('ja_ginza') doc = nlp(content)
analyze.py の create_vocabulary_list を見ると最初は spacy ライブラリーからja_ginzaというモデルを持ってきます。 その結果の nlp というモデルに日本語の文書(content)を入力して doc という変数を得ます。
... for sentence in doc.sents: for token in sentence: if token.pos_ == 'PUNCT' or token.pos_ == 'ADP' or token.pos_ == 'SCONJ' or token.pos_ == 'NUM': continue elif token.pos_ == 'AUX' and token.dep_ == 'aux': continue elif re.match("^[A-Za-z]*$", token.orth_): continue ...
token は形態素分析の結果です。より簡単に理解する為に以下のコードを実行して結果を貼ります。
#!/usr/bin/python # -*- coding: utf-8 -*- import spacy def create_vocabulary_list(content): nlp = spacy.load('ja_ginza') doc = nlp(content) vocab_list = [] for sentence in doc.sents: for token in sentence: print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i) if __name__ == '__main__': content = '本エントリでは、サブスクリプションフィルタを利用せず、CloudWatch Logsのロググループを、S3バケットにエクスポートしてみたいと思います。' vocab_list = create_vocabulary_list(content)
この結果を見ると
2 で で ADP 助詞-格助詞 case 1 3 は は ADP 助詞-係助詞 case 1 4 、 、 PUNCT 補助記号-読点 punct 1
'で'、'は'、'、' みたいなことは日本語辞書のターゲット単語の対象外にした方が良いと思いましたので if elif で除きました。 同様の理由で英語も regexp で除きました。
3. JSONファイルで保存
次は先ほど纏めた vocab_list をJSONファイルにて保存します。
$ touch save.py
save.py
#!/usr/bin/python # -*- coding: utf-8 -*- import os import uuid import json from datetime import date from vocabulary import Vocabulary def generate_filename(): today = date.today().strftime('%Y%m%d') postfix = str(uuid.uuid4()).replace('-', '') filename = today + '_' + postfix + '.json' return filename def save_file(filename, vocab_list): if len(vocab_list) > 0: dirname = filename[0:8] try: os.mkdir(os.path.join(os.path.dirname(__file__), '../generated/' + dirname)) except FileExistsError: print('directory "' + dirname + '" already exists') filepath = os.path.join(os.path.dirname(__file__), '../generated/' + dirname + '/' + filename) with open(filepath, 'w', encoding='utf8') as json_file: json_file.write('[') for vocab in vocab_list: json.dump(vocab.__dict__, json_file, ensure_ascii=False) json_file.write(',') json_file.seek(json_file.tell() - 1, os.SEEK_SET) json_file.write(']') if __name__ == '__main__': filename = generate_filename() save_file(filename, [])
このファイルは ${PROJECT_ROOT}/generated/20190919/20190919_bc6849a04abc44eaa30b82b7f0dbc2df.json のような形で保存されます。 これらを集めて次のステップで DynamoDB にアップロードする流れになります。
今までのコードが三つのファイルに分けてあって、纏めて実行する方が楽なので纏めて実行してみました。
$ touch main.py
main.py
#!/usr/bin/python # -*- coding: utf-8 -*- from parse import get_html_doc, get_blog_content from analyze import create_vocabulary_list from save import generate_filename, save_file url = input('Dev.IO Page URL : ') html_doc = get_html_doc(url) content = get_blog_content(html_doc) vocab_list = create_vocabulary_list(content) filename = generate_filename() save_file(filename, vocab_list)
すると以下のようなコマンドで、ブログの内容をテキストで抽出、形態素分析、JSON形式で保存までの流れが自動化されます。
$ python src/main.py
4. JSONファイルのデータをDynamoDBにアップロード
コードを作成する前、aws cli で DynamoDBのテーブルを作るスクリプトを作ります。
$ cd .. $ echo aws dynamodb create-table --table-name JapaneseVocabulary --key-schema AttributeName=lemma,KeyType=HASH AttributeName=timestamp,KeyType=RANGE --attribute-definitions AttributeName=lemma,AttributeType=S AttributeName=timestamp,AttributeType=S --billing-mode PAY_PER_REQUEST > create_resources.sh $ sh create_resources.sh
作業中のディレクトリ(src)に戻って upload.py ファイルを作ります。
$ cd src $ touch upload.py
upload.py
#!/usr/bin/python # -*- coding: utf-8 -*- import boto3 import json import os import datetime from boto3.dynamodb.conditions import Key, Attr from botocore.exceptions import ClientError def read_vocab_files(): cwd = os.path.dirname(__file__) generated_dirname = cwd + '/../generated' if cwd == 'src' else '../generated' target_dirname = os.path.abspath( generated_dirname + '/' + datetime.date.today().strftime('%Y%m%d') + '/') vocab_files = os.listdir(target_dirname) vocab_files = [ target_dirname + '/' + f for f in vocab_files if os.path.isfile(os.path.join(target_dirname, f))] vocab_list = [] for filepath in vocab_files: with open(filepath, 'r') as f: json_data = json.load(f) vocab_list = vocab_list + json_data return vocab_list def batch_write_item(table, vocab_list): try: with table.batch_writer() as batch: for vocab in vocab_list: batch.put_item(Item=vocab) except ClientError as e: print(e.response['Error']['Message']) def add_vocab_list(vocab_list): dynamodb = boto3.resource("dynamodb", region_name='ap-northeast-1') table = dynamodb.Table('JapaneseVocabulary') batch_write_item(table, vocab_list) if __name__ == '__main__': vocab_list = read_vocab_files() add_vocab_list(vocab_list) print('done.')
これを実行してみると日本語辞書が出来上がります!
5. クエリを実行・結果確認
最後に簡単にクエリを実行してみることで作業結果を確認してみます。
$ touch search.py
search.py
#!/usr/bin/python # -*- coding: utf-8 -*- import boto3 import json import os import datetime from boto3.dynamodb.conditions import Key, Attr from botocore.exceptions import ClientError def search_vocab(keyword): dynamodb = boto3.resource("dynamodb", region_name='ap-northeast-1') table = dynamodb.Table('JapaneseVocabulary') response = table.query( KeyConditionExpression=Key('lemma').eq(keyword) ) result = response['Items'] return result def print_result(result): print('------ SEARCH RESULT ------') for r in result: print(r['lemma']) print(r['sentence']) print() if __name__ == '__main__': keyword = input('search keyword: ') result = search_vocab(keyword) print_result(result)
最後に
日本語の勉強を加速化する為の対策として自分の日本語辞書を実装してみました。 これを作ってみたら確かに有用なところがあることが分かりました。
例えば、私は新しい単語を覚えても、その使い方が誤っていたのが多いです。 自分では感じられませんが、多分このブログの中にも違和感のある文章が多いと存じます。 その問題を改善する為にはやはり日本語が母国語である方の文章を丸で覚えた方が良いですよね。 私の辞書は単語とその単語が使われた文書も一緒に保存していて、そういうところに関しては楽だと思います。
これからもこのプロジェクトをガンガン発展して行きたいと思いながら本記事を纏めます。